function forEach(array, action) {
	for (var i = 0; i < array.length; i++) {
		action(array[i]);
	}
}

function forEachIn(obj, func) {
	for (var prop in obj) {
		if (Object.prototype.hasOwnProperty.call(obj, prop) && Object.prototype.propertyIsEnumerable.call(obj, prop)) {
			func(prop, obj[prop]);
		}
	}
}


function bind(func, object) {
	return function(){
		return func.apply(object, arguments);
	};
}

/*
 * This is one type of inheritence
 */
function clone(object) {
	function OneShotConstructor(){}
	OneShotConstructor.prototype = object;
	return new OneShotConstructor();
}

/*
 * Class Dictionary
 */
function Dictionary (initValues) {
	this.values = initValues || {};
}

Dictionary.prototype.store = function (name, value) {
	this.values[name] = value;
}

Dictionary.prototype.lookup = function (name) {
	return this.values[name];
}

Dictionary.prototype.contains = function (name) {
	return Object.prototype.hasOwnProperty.call(this.values, name) && Object.prototype.propertyIsEnumerable.call(this.values, name);
}

Dictionary.prototype.each = function (action) {
	forEachIn(this.values, action);
}

var directions = new Dictionary(
		{"n":  new Point( 0, -1),
	     "ne": new Point( 1, -1),
		 "e":  new Point( 1,  0),
		 "se": new Point( 1,  1),
		 "s":  new Point( 0,  1),
		 "sw": new Point(-1,  1),
		 "w":  new Point(-1,  0),
		 "nw": new Point(-1, -1)}
	);

var creatureTypes = new Dictionary();

creatureTypes.register = function(constructor) {
	this.store(constructor.prototype.character, constructor);
};

var thePlan =
  ["############################",
   "#      #    #      o      ##",
   "#                          #",
   "#          #####           #",
   "##         #   #    ##     #",
   "###           ##     #     #",
   "#           ###      #     #",
   "#   ####                   #",
   "#   ##       o             #",
   "# o  #         o       ### #",
   "#    #                     #",
   "############################"];

var newPlan =
	  ["############################",
	   "#                      #####",
	   "#    ##                 ####",
	   "#   ####     ~ ~          ##",
	   "#    ##       ~            #",
	   "#                          #",
	   "#                ###       #",
	   "#               #####      #",
	   "#                ###       #",
	   "# %        ###        %    #",
	   "#        #######           #",
	   "############################"];

var lichenPlan =
	  ["############################",
	   "#                     ######",
	   "#    ***                **##",
	   "#   *##**         **  c  *##",
	   "#    ***     c    ##**    *#",
	   "#       c         ##***   *#",
	   "#                 ##**    *#",
	   "#   c       #*            *#",
	   "#*          #**       c   *#",
	   "#***        ##**    c    **#",
	   "#*****     ###***       *###",
	   "############################"];

/*
 * Class Point
 * 
 */
function Point (x, y) {
	this.x = x;
	this.y = y;
}

Point.prototype.add = function (point) {
	return new Point(this.x + point.x, this.y + point.y);
}
	  

Point.prototype.isEqualTo = function (point) {
	return this.x === point.x && this.y === point.y;
}

Point.prototype.toString = function() {
	return "(" + this.x + "," + this.y + ")";
};

/*
 * Class Grid
 * 
 */
function Grid (width, height) {
	this.width = width;
	this.height = height;
	
	this.cells = new Array(width * height);
	
}

function characterFromElement(element) {
	if (element == undefined)
		return " ";
	else
	    return element.character;
}

Grid.prototype.valueAt = function (point) {
	return this.cells[point.x + point.y * this.width];
}

Grid.prototype.setValueAt = function(point, value) {
	this.cells[point.x + point.y * this.width] = value;
}

Grid.prototype.isInside = function(point) {
	return point.x >=0 && point.y >= 0 && point.x < this.width && point.y < this.height;
}

Grid.prototype.moveValue = function(from, to) {
	this.setValueAt(to, this.valueAt(from));
	this.setValueAt(from, undefined);
}

Grid.prototype.each = function (action) {
	for (var y = 0; y < this.height; y++) {
		for (var x = 0; x < this.width; x++) {
			var point = new Point(x, y);
			action(point, this.valueAt(point));
		}
	}
}

/*
 * Bugs
 */


function randomElement(array) {
	  if (array.length == 0)
	    throw new Error("The array is empty.");
	  return array[Math.floor(Math.random() * array.length)];
}

function findDirections(surroundings, wanted) {
	var found = [];
	directions.each(function(name) {
	if (surroundings[name] == wanted)
		found.push(name);
	 });
	return found;
}

function StupidBug() {
};
StupidBug.prototype.character = "o";
StupidBug.prototype.act = function(surroundings) {
	return {type: "move", direction: "s"};
};

function BouncingBug() {
	  this.direction = "ne";
}
BouncingBug.prototype.character = "%";	
BouncingBug.prototype.act = function(surroundings) {
	if (surroundings[this.direction] != " ")
	    this.direction = (this.direction == "ne" ? "sw" : "ne");
	return {type: "move", direction: this.direction};
};

function DrunkBug() {
	
}
DrunkBug.prototype.character = "~";
DrunkBug.prototype.act = function(surroundings) {
	var values = Object.keys(directions.values);
	return {type: "move", direction: values[Math.floor(Math.random() * values.length)]};
}

function Lichen() {
	this.energy = 5;
}
Lichen.prototype.act = function(surroundings) {
	var emptySpace = findDirections(surroundings, " ");
	if (this.energy >= 13 && emptySpace.length > 0)
		return {type: "reproduce", direction: randomElement(emptySpace)};
	else if (this.energy < 20)
		return {type: "photosynthese"};
	else
		return {type: "wait"};
};
Lichen.prototype.character = "*";

function LichenEater() {
	this.energy = 10;
}
LichenEater.prototype.act = function(surroundings) {
	var emptySpace = findDirections(surroundings, " ");
	var lichen = findDirections(surroundings, "*");

	if (this.energy >= 30 && emptySpace.length > 0)
		return {type: "reproduce", direction: randomElement(emptySpace)};
	else if (lichen.length > 0)
		return {type: "eat", direction: randomElement(lichen)};
	else if (emptySpace.length > 0)
		return {type: "move", direction: randomElement(emptySpace)};
	else
		return {type: "wait"};
};
LichenEater.prototype.character = "c";

creatureTypes.register(LichenEater);	
creatureTypes.register(Lichen);
creatureTypes.register(StupidBug);
creatureTypes.register(BouncingBug);
creatureTypes.register(DrunkBug);


var wall = {
		character: "#"
};

/*
 * Class Terrarium
 */
function Terrarium(plan) {
	var grid = new Grid(plan[0].length, plan.length);
	for (var y = 0; y < plan.length; y++) {
		var line = plan[y];
		for (var x = 0; x < line.length; x++) {
			var point = new Point(x, y);
			grid.setValueAt(point, elementFromCharacter(line.charAt(x)));
		}
	}
	
	this.grid = grid;
	
}

function elementFromCharacter(character) {
	if (character == " ")
		return undefined;
	else if (character == "#")
	    return wall;
	else if (creatureTypes.contains(character))
	    return new (creatureTypes.lookup(character))();
	else 
		throw new Error("Unknown character: " + character);
}

Terrarium.prototype.toString = function() {
	var characters = [];
	var endOfLine = this.grid.width - 1;
	this.grid.each(function(point, value) {
		characters.push(characterFromElement(value));
	    if (point.x == endOfLine)
	      characters.push("\n");
	});
	return characters.join("");
};

Terrarium.prototype.listActingCreatures = function() {
	
	function Creature(value, point) {
		this.object = value; 
		this.point =point;
	}
	
	var found = [];
	
	this.grid.each(function (point, value){
		if (value != undefined && value.act) 
			found.push(new Creature(value, point));
	});
	
	return found;
}

Terrarium.prototype.listSurroundings = function (point) {
	var thatGrid = this.grid;
	var surrounding = {};
	directions.each(function (name, value){
		var newPoint = point.add(value);
		
		if (thatGrid.isInside(newPoint)) {
			surrounding[name] = characterFromElement(thatGrid.valueAt(newPoint));
		} else
			surrounding[name] = "#";
	});
	
	return surrounding;
}

Terrarium.prototype.processCreature = function(creature) {
	var surroundings = this.listSurroundings(creature.point);
	var action = creature.object.act(surroundings);
	if (action.type == "move" && directions.contains(action.direction)) {
		var to = creature.point.add(directions.lookup(action.direction));
		if (this.grid.isInside(to) && this.grid.valueAt(to) == undefined) {
			this.grid.moveValue(creature.point, to);
		}
	} else {
	    throw new Error("Unsupported action: " + action.type);
	}
};

Terrarium.prototype.step = function() {
	forEach(this.listActingCreatures(), bind(this.processCreature, this));
	
	if (this.onStep)
	    this.onStep();
};

Terrarium.prototype.start = function() {
	if (!this.running)
		this.running = setInterval(bind(this.step, this), 500);
};

Terrarium.prototype.stop = function() {
	if (this.running) {
		clearInterval(this.running);
	    this.running = null;
	}
};

function LifeLikeTerrarium(plan) {
	Terrarium.call(this, plan);
}
LifeLikeTerrarium.prototype = clone(Terrarium.prototype);
LifeLikeTerrarium.prototype.constructor = LifeLikeTerrarium;

LifeLikeTerrarium.prototype.processCreature = function(creature) {
	var surroundings = this.listSurroundings(creature.point);
	var action = creature.object.act(surroundings);

	var target = undefined;
	var valueAtTarget = undefined;
	if (action.direction && directions.contains(action.direction)) {
		var direction = directions.lookup(action.direction);
	    var maybe = creature.point.add(direction);
	    if (this.grid.isInside(maybe)) {
	      target = maybe;
	      valueAtTarget = this.grid.valueAt(target);
	    }
	}

	if (action.type == "move") {
		if (target && !valueAtTarget) {
	      this.grid.moveValue(creature.point, target);
	      creature.point = target;
	      creature.object.energy -= 1;
	    }
	}
	else if (action.type == "eat") {
	    if (valueAtTarget && valueAtTarget.energy) {
	    	this.grid.setValueAt(target, undefined);
	    	creature.object.energy += valueAtTarget.energy;
	    }
	}
	else if (action.type == "photosynthese") {
		creature.object.energy += 1;
	}
	else if (action.type == "reproduce") {
		if (target && !valueAtTarget) {
			var species = characterFromElement(creature.object);
			var baby = elementFromCharacter(species);
			creature.object.energy -= baby.energy * 2;
			if (creature.object.energy > 0)
				this.grid.setValueAt(target, baby);
		}
	}
	else if (action.type == "wait") {
	    creature.object.energy -= 0.2;
	}
	else {
		throw new Error("Unsupported action: " + action.type);
	}

	if (creature.object.energy <= 0)
		this.grid.setValueAt(creature.point, undefined);
};

var terrarium = new LifeLikeTerrarium(lichenPlan);
terrarium.onStep = partial(inPlacePrinter(), terrarium);
terrarium.start();